#include "name_lock.h"

#include <unistd.h>
#include <sys/file.h>

#include "fs.h"
#include "stopwatch.h"
#include "timeit.h"
#include "exception.h"
#include "log.h"


namespace evm {

InterProcessLock::~InterProcessLock() {
    try {
        release();
    } catch (const exception& e) {
        /* Do not throw in destructor. */
        log_error(e.what());
    }
}

bool InterProcessLock::acquire(bool block, long timeout, double incr_delay, double max_delay) {
    if (acquired_)
        return true;

    open();

    // non-blocking
    if (!block) {
        acquired_ = lock();
        return acquired_;
    }

    // blocking
    auto watch = StopWatch(timeout * 1000);
    double delay = 0;
    while (!watch.expired()) {
        sleep(delay);
        delay = std::max(delay + incr_delay, max_delay);

        acquired_ = lock();
        if (acquired_)
            return true;
    }
    return false;
}

void InterProcessLock::release() {
    if (acquired_) {
        unlock();

        acquired_ = false;

        close();
    }
}

void InterProcessLock::open() {
    string basedir = fs::path(path_).parent_path();
    if (!basedir.empty()) {
        bool created = false;

        if (!fs::exists(basedir))
            created = fs::create_directories(basedir);

        if (!fs::exists(basedir) || !fs::is_directory(basedir))
            throw runtime_error("Create lock base path failed: " + basedir);

        if (created)
            log_info("Create lock base path: " + basedir);
    }

    if (fd_ == -1) {
        fd_ = ::open(path_.c_str(), O_CREAT | O_APPEND);
        check_unix_error(fd_, "open");
    }

}

void InterProcessLock::close() {
    ::close(fd_);
    fd_ = -1;
}

bool InterProcessLock::lock() {
    if (::flock(fd_, LOCK_EX | LOCK_NB) == -1) {  // non-blocking flock
        if (errno == EWOULDBLOCK)  // file has been locked by others
            return false;
        else
            throw_system_error("flock, LOCK_EX | LOCK_NB");
    }
    return true;
}

void InterProcessLock::unlock() {
    check_unix_error(::flock(fd_, LOCK_UN), "flock, LOCK_UN");
}

NameLock::NameLock(const string &name, const string &lock_path)
    : NameLock(name, make_shared<InterProcessLock>(fs::path(lock_path) / name)) {}

NameLock::NameLock(const string &name, shared_ptr<InterProcessLock> process_lock)
    : namelock_(new _NameLock(name)), process_lock_(process_lock)
{
    lock();

    try {
        if (process_lock_)
            process_lock_->acquire();  // blocking acquire lock
    } catch (const exception& e) {
        log_error(e.what());
        /* Acquired name lock should be released,
         * since the destructor won't be called after throwing. */
        unlock();
        throw;
    }
}

NameLock::~NameLock() {
    try {
        if (process_lock_)
            process_lock_->release();
    } catch (const exception& e) {
        /* Do not throw in destructor. */
        log_error(e.what());
    }

    unlock();
}

unordered_set<NameLock::_NameLockPtr, NameLock::hash, NameLock::equal> NameLock::namelocks_;
mutex NameLock::namelocks_mutex_;

void NameLock::lock() {
    {
        unique_lock<mutex> ul(namelocks_mutex_);

        auto iter = namelocks_.find(namelock_);
        if (iter == namelocks_.end())
            namelocks_.insert(namelock_);
        else
            namelock_ = *iter;
    }

    namelock_->mutex_.lock();
}

void NameLock::unlock() {
    namelock_->mutex_.unlock();

    {
        unique_lock<mutex> ul(namelocks_mutex_);

        auto iter = namelocks_.find(namelock_);
        if (iter != namelocks_.end()) {
            if (namelock_.use_count() <= 2)  // if no others reference this name
                namelocks_.erase(iter);
        }
    }
}

void NameLock::remove_lock_file(const string& name, const string& lock_path) {
    NameLock nl(name);
    string lock_file = fs::path(lock_path) / name;
    try {
        fs::remove(lock_file, true);
    } catch (const fs::filesystem_error& e) {
        log_error("Failed to remove file {}", lock_file);
    }
}

vector<string> NameLock::names() {
    unique_lock<mutex> ul(namelocks_mutex_);

    vector<string> ns;
    string log;
    for (auto& nl: namelocks_) {
        ns.push_back(nl->name_);
        log += " " + nl->name_ + ":" + std::to_string(nl.use_count());
    }
    log_info("Global name locks:{}", log);
    return std::move(ns);
}

}
